baubs git
src/formats/pen/encoder.ts fix/state-during-render-usePatternTransform (47e32ef8) Text, 13.20 KB
/**
* PEN Format Encoder
*
* This module contains the logic for encoding embroidery stitches into the Brother PP1 PEN format.
* The PEN format uses absolute coordinates shifted left by 3 bits, with flags in the low 3 bits.
*/
import { MOVE, TRIM, END } from "../import/constants";
// PEN format flags for Brother machines
const PEN_FEED_DATA = 0x01; // Bit 0: Jump stitch (move without stitching)
const PEN_CUT_DATA = 0x02; // Bit 1: Trim/cut thread command
const PEN_COLOR_END = 0x03; // Last stitch before color change
const PEN_DATA_END = 0x05; // Last stitch of entire pattern
// Constants from PesxToPen.cs
const FEED_LENGTH = 50; // Long jump threshold requiring lock stitches and cut
const TARGET_LENGTH = 8.0; // Target accumulated length for lock stitch direction
const MAX_POINTS = 5; // Maximum points to accumulate for lock stitch direction
export const LOCK_STITCH_JUMP_SIZE = 2.0;
const LOCK_STITCH_SCALE = LOCK_STITCH_JUMP_SIZE / 8.0; // Scale the magnitude-8 vector down to 4
export interface StitchData {
x: number;
y: number;
cmd: number;
colorIndex: number;
}
export interface PenEncodingResult {
penBytes: number[];
bounds: {
minX: number;
maxX: number;
minY: number;
maxY: number;
};
}
/**
* Encode a stitch position to PEN bytes (4 bytes: X_low, X_high, Y_low, Y_high)
* Coordinates are shifted left by 3 bits to make room for flags in low 3 bits
*/
export function encodeStitchPosition(x: number, y: number): number[] {
const xEnc = (Math.round(x) << 3) & 0xffff;
const yEnc = (Math.round(y) << 3) & 0xffff;
return [xEnc & 0xff, (xEnc >> 8) & 0xff, yEnc & 0xff, (yEnc >> 8) & 0xff];
}
/**
* Calculate lock stitch direction by accumulating movement vectors
* Matches the C# logic that accumulates coordinates until reaching threshold
*
* Three use cases from C# ConvertEmb function:
* - Loop A (Jump/Entry): lookAhead=true - Hides knot under upcoming stitches
* - Loop B (End/Cut): lookAhead=false - Hides knot inside previous stitches
* - Loop C (Color Change): lookAhead=true - Aligns knot with stop event data
*
* @param stitches Array of stitches to analyze [x, y, cmd, colorIndex]
* @param currentIndex Current stitch index
* @param lookAhead If true, look forward; if false, look backward
* @returns Direction vector components (normalized and scaled to magnitude 8.0)
*/
export function calculateLockDirection(
stitches: number[][],
currentIndex: number,
lookAhead: boolean,
): { dirX: number; dirY: number } {
let accumulatedX = 0;
let accumulatedY = 0;
let maxLength = 0;
let bestX = 0;
let bestY = 0;
const step = lookAhead ? 1 : -1;
const maxIterations = lookAhead
? Math.min(MAX_POINTS, stitches.length - currentIndex - 1)
: Math.min(MAX_POINTS, currentIndex);
for (let i = 0; i < maxIterations; i++) {
const idx = currentIndex + step * (i + 1);
if (idx < 0 || idx >= stitches.length) break;
const stitch = stitches[idx];
const cmd = stitch[2];
// Skip MOVE/JUMP stitches
if ((cmd & MOVE) !== 0) continue;
// Accumulate relative coordinates
const deltaX =
Math.round(stitch[0]) - Math.round(stitches[currentIndex][0]);
const deltaY =
Math.round(stitch[1]) - Math.round(stitches[currentIndex][1]);
accumulatedX += deltaX;
accumulatedY += deltaY;
const length = Math.sqrt(
accumulatedX * accumulatedX + accumulatedY * accumulatedY,
);
// Track the maximum length vector seen so far
if (length > maxLength) {
maxLength = length;
bestX = accumulatedX;
bestY = accumulatedY;
}
// If we've accumulated enough length, use current vector
if (length >= TARGET_LENGTH) {
return {
dirX: (accumulatedX * 8.0) / length,
dirY: (accumulatedY * 8.0) / length,
};
}
}
// If we didn't reach target length, use the best vector we found
if (maxLength > 0.1) {
return {
dirX: (bestX * 8.0) / maxLength,
dirY: (bestY * 8.0) / maxLength,
};
}
// Fallback: diagonal direction with magnitude 8.0
const mag = 8.0 / Math.sqrt(2); // ~5.66 for diagonal
return { dirX: mag, dirY: mag };
}
/**
* Generate lock/tack stitches at a position, rotated toward the direction of travel
* Matches Nuihajime_TomeDataPlus from PesxToPen.cs with vector rotation
* @param x X coordinate
* @param y Y coordinate
* @param dirX Direction X component (magnitude ~8.0)
* @param dirY Direction Y component (magnitude ~8.0)
* @returns Array of PEN bytes for lock stitches (32 bytes = 8 stitches * 4 bytes)
*/
export function generateLockStitches(
x: number,
y: number,
dirX: number,
dirY: number,
): number[] {
const lockBytes: number[] = [];
// Generate 8 lock stitches in alternating pattern
// Pattern from C# (from Nuihajime_TomeDataPlus): [+x, +y, -x, -y] repeated
// The direction vector has magnitude ~8.0, so we need to scale it down
// to get reasonable lock stitch size (approximately 0.4 units)
const scaledDirX = dirX * LOCK_STITCH_SCALE;
const scaledDirY = dirY * LOCK_STITCH_SCALE;
// Generate 8 stitches alternating between forward and backward
for (let i = 0; i < 8; i++) {
// Alternate between forward (+) and backward (-) direction
const sign = i % 2 === 0 ? 1 : -1;
const xAdd = scaledDirX * sign;
const yAdd = scaledDirY * sign;
lockBytes.push(...encodeStitchPosition(x + xAdd, y + yAdd));
}
return lockBytes;
}
/**
* Encode stitches array to PEN format bytes
*
* @param stitches Array of stitches in format [x, y, cmd, colorIndex]
* @returns PEN encoding result with bytes and bounds
*/
export function encodeStitchesToPen(stitches: number[][]): PenEncodingResult {
// Track bounds
let minX = Infinity;
let maxX = -Infinity;
let minY = Infinity;
let maxY = -Infinity;
const penStitches: number[] = [];
// Track position for calculating jump distances
let prevX = 0;
let prevY = 0;
for (let i = 0; i < stitches.length; i++) {
const stitch = stitches[i];
const absX = Math.round(stitch[0]);
const absY = Math.round(stitch[1]);
const cmd = stitch[2];
const stitchColor = stitch[3]; // Color index from PyStitch
// Track bounds for non-jump stitches (regular stitches, not MOVE/JUMP)
// A stitch is trackable if it's not a MOVE command
if ((cmd & MOVE) === 0) {
minX = Math.min(minX, absX);
maxX = Math.max(maxX, absX);
minY = Math.min(minY, absY);
maxY = Math.max(maxY, absY);
}
const isFirstStitch = i == 0;
// Check for long jumps that need lock stitches and cuts
if (cmd & MOVE) {
const jumpDist = Math.sqrt((absX - prevX) ** 2 + (absY - prevY) ** 2);
if (!isFirstStitch && jumpDist > FEED_LENGTH) {
// Long jump - add finishing lock stitches at previous position
// Loop B: End/Cut Vector - Look BACKWARD at previous stitches
// This hides the knot inside the embroidery we just finished
const finishDir = calculateLockDirection(stitches, i - 1, false);
penStitches.push(
...generateLockStitches(prevX, prevY, finishDir.dirX, finishDir.dirY),
);
// Encode jump with both FEED and CUT flags
const xEncoded = (absX << 3) & 0xffff;
let yEncoded = (absY << 3) & 0xffff;
yEncoded |= PEN_FEED_DATA; // Jump flag
yEncoded |= PEN_CUT_DATA; // Cut flag for long jumps
penStitches.push(
xEncoded & 0xff,
(xEncoded >> 8) & 0xff,
yEncoded & 0xff,
(yEncoded >> 8) & 0xff,
);
// Add starting lock stitches at new position
// Loop A: Jump/Entry Vector - Look FORWARD at upcoming stitches
// This hides the knot under the stitches we're about to make
const startDir = calculateLockDirection(stitches, i, true);
penStitches.push(
...generateLockStitches(absX, absY, startDir.dirX, startDir.dirY),
);
// Update position and continue
prevX = absX;
prevY = absY;
continue;
}
}
// Encode absolute coordinates with flags in low 3 bits
// Shift coordinates left by 3 bits to make room for flags
let xEncoded = (absX << 3) & 0xffff;
let yEncoded = (absY << 3) & 0xffff;
// Add command flags to Y-coordinate based on stitch type
if (cmd & MOVE) {
// MOVE/JUMP: Set bit 0 (FEED_DATA) - move without stitching
yEncoded |= PEN_FEED_DATA;
}
if (cmd & TRIM) {
// TRIM: Set bit 1 (CUT_DATA) - cut thread command
yEncoded |= PEN_CUT_DATA;
}
// Check if this is the last stitch
const isLastStitch = i === stitches.length - 1 || (cmd & END) !== 0;
// Check for color change by comparing stitch color index
const nextStitch = stitches[i + 1];
const nextStitchColor = nextStitch?.[3];
const isColorChange =
!isLastStitch &&
nextStitchColor !== undefined &&
nextStitchColor !== stitchColor;
// Mark the very last stitch of the pattern with DATA_END
if (isLastStitch) {
xEncoded = (xEncoded & 0xfff8) | PEN_DATA_END;
}
// Add the encoded stitch
penStitches.push(
xEncoded & 0xff,
(xEncoded >> 8) & 0xff,
yEncoded & 0xff,
(yEncoded >> 8) & 0xff,
);
// Update position for next iteration
prevX = absX;
prevY = absY;
if (isFirstStitch) {
// Add starting lock stitches at the very beginning of the pattern
// Matches C# behavior: Nuihajime_TomeDataPlus is called when counter <= 2
// Calculate direction for starting locks (look forward into the pattern)
const startDir = calculateLockDirection(stitches, i, true);
penStitches.push(
...generateLockStitches(absX, absY, startDir.dirX, startDir.dirY),
);
}
// Handle color change: finishing lock, COLOR_END+CUT, jump, starting lock
if (isColorChange) {
const nextStitchCmd = nextStitch[2];
const nextStitchX = Math.round(nextStitch[0]);
const nextStitchY = Math.round(nextStitch[1]);
const nextIsJump = (nextStitchCmd & MOVE) !== 0;
// Step 1: Add finishing lock stitches at end of current color
// Loop C: Color Change Vector - Look FORWARD at the stop event data
// This aligns the knot with the stop command's data block for correct tension
const finishDir = calculateLockDirection(stitches, i, true);
penStitches.push(
...generateLockStitches(absX, absY, finishDir.dirX, finishDir.dirY),
);
// Step 2: Add COLOR_END + CUT command at CURRENT position (same stitch!)
// This is where the machine pauses and waits for the user to change thread color
// IMPORTANT: COLOR_END and CUT must be on the SAME stitch, not separate stitches
let colorEndCutXEncoded = (absX << 3) & 0xffff;
let colorEndCutYEncoded = (absY << 3) & 0xffff;
// Add COLOR_END flag to X coordinate and CUT flag to Y coordinate
colorEndCutXEncoded = (colorEndCutXEncoded & 0xfff8) | PEN_COLOR_END;
colorEndCutYEncoded |= PEN_CUT_DATA;
penStitches.push(
colorEndCutXEncoded & 0xff,
(colorEndCutXEncoded >> 8) & 0xff,
colorEndCutYEncoded & 0xff,
(colorEndCutYEncoded >> 8) & 0xff,
);
// Machine pauses here for color change
// After user changes color, the following stitches execute with the new color
// Step 3: If next stitch is a JUMP, encode it and skip it in the loop
// Otherwise, add a jump ourselves if positions differ
const jumpToX = nextStitchX;
const jumpToY = nextStitchY;
if (nextIsJump) {
// The PES has a JUMP to the new color position, we'll add it here and skip it later
i++; // Skip the JUMP stitch since we're processing it here
}
// Add jump to new position (if position changed)
if (jumpToX !== absX || jumpToY !== absY) {
const jumpXEncoded = (jumpToX << 3) & 0xffff;
let jumpYEncoded = (jumpToY << 3) & 0xffff;
jumpYEncoded |= PEN_FEED_DATA; // Jump flag
penStitches.push(
jumpXEncoded & 0xff,
(jumpXEncoded >> 8) & 0xff,
jumpYEncoded & 0xff,
(jumpYEncoded >> 8) & 0xff,
);
}
// Step 4: Add starting lock stitches at the new position
// Loop A: Jump/Entry Vector - Look FORWARD at upcoming stitches in new color
// This hides the knot under the stitches we're about to make
const nextStitchIdx = nextIsJump ? i + 2 : i + 1;
const startDir = calculateLockDirection(
stitches,
nextStitchIdx < stitches.length ? nextStitchIdx : i,
true,
);
penStitches.push(
...generateLockStitches(jumpToX, jumpToY, startDir.dirX, startDir.dirY),
);
// Update position
prevX = jumpToX;
prevY = jumpToY;
}
// Check for end command
if ((cmd & END) !== 0) {
break;
}
}
return {
penBytes: penStitches,
bounds: {
minX: minX === Infinity ? 0 : minX,
maxX: maxX === -Infinity ? 0 : maxX,
minY: minY === Infinity ? 0 : minY,
maxY: maxY === -Infinity ? 0 : maxY,
},
};
}
Served by rngit 1.3.3 - Generated in 0.03s